{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T18:54:58.722373Z",
     "start_time": "2021-01-13T18:54:57.178438Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Populating the interactive namespace from numpy and matplotlib\n"
     ]
    }
   ],
   "source": [
    "%pylab inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Notebook magic"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T18:55:01.909310Z",
     "start_time": "2021-01-13T18:55:01.903634Z"
    }
   },
   "outputs": [],
   "source": [
    "from IPython.core.magic import Magics, magics_class, line_cell_magic\n",
    "from IPython.core.magic import cell_magic, register_cell_magic, register_line_magic\n",
    "from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring\n",
    "import subprocess\n",
    "import os"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T18:55:02.434518Z",
     "start_time": "2021-01-13T18:55:02.382296Z"
    }
   },
   "outputs": [],
   "source": [
    "@magics_class\n",
    "class PyboardMagic(Magics):\n",
    "    @cell_magic\n",
    "    @magic_arguments()\n",
    "    @argument('-skip')\n",
    "    @argument('-unix')\n",
    "    @argument('-pyboard')\n",
    "    @argument('-file')\n",
    "    @argument('-data')\n",
    "    @argument('-time')\n",
    "    @argument('-memory')\n",
    "    def micropython(self, line='', cell=None):\n",
    "        args = parse_argstring(self.micropython, line)\n",
    "        if args.skip: # doesn't care about the cell's content\n",
    "            print('skipped execution')\n",
    "            return None # do not parse the rest\n",
    "        if args.unix: # tests the code on the unix port. Note that this works on unix only\n",
    "            with open('/dev/shm/micropython.py', 'w') as fout:\n",
    "                fout.write(cell)\n",
    "            proc = subprocess.Popen([\"../../micropython/ports/unix/micropython\", \"/dev/shm/micropython.py\"], \n",
    "                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
    "            print(proc.stdout.read().decode(\"utf-8\"))\n",
    "            print(proc.stderr.read().decode(\"utf-8\"))\n",
    "            return None\n",
    "        if args.file: # can be used to copy the cell content onto the pyboard's flash\n",
    "            spaces = \"    \"\n",
    "            try:\n",
    "                with open(args.file, 'w') as fout:\n",
    "                    fout.write(cell.replace('\\t', spaces))\n",
    "                    printf('written cell to {}'.format(args.file))\n",
    "            except:\n",
    "                print('Failed to write to disc!')\n",
    "            return None # do not parse the rest\n",
    "        if args.data: # can be used to load data from the pyboard directly into kernel space\n",
    "            message = pyb.exec(cell)\n",
    "            if len(message) == 0:\n",
    "                print('pyboard >>>')\n",
    "            else:\n",
    "                print(message.decode('utf-8'))\n",
    "                # register new variable in user namespace\n",
    "                self.shell.user_ns[args.data] = string_to_matrix(message.decode(\"utf-8\"))\n",
    "        \n",
    "        if args.time: # measures the time of executions\n",
    "            pyb.exec('import utime')\n",
    "            message = pyb.exec('t = utime.ticks_us()\\n' + cell + '\\ndelta = utime.ticks_diff(utime.ticks_us(), t)' + \n",
    "                               \"\\nprint('execution time: {:d} us'.format(delta))\")\n",
    "            print(message.decode('utf-8'))\n",
    "        \n",
    "        if args.memory: # prints out memory information \n",
    "            message = pyb.exec('from micropython import mem_info\\nprint(mem_info())\\n')\n",
    "            print(\"memory before execution:\\n========================\\n\", message.decode('utf-8'))\n",
    "            message = pyb.exec(cell)\n",
    "            print(\">>> \", message.decode('utf-8'))\n",
    "            message = pyb.exec('print(mem_info())')\n",
    "            print(\"memory after execution:\\n========================\\n\", message.decode('utf-8'))\n",
    "\n",
    "        if args.pyboard:\n",
    "            message = pyb.exec(cell)\n",
    "            print(message.decode('utf-8'))\n",
    "\n",
    "ip = get_ipython()\n",
    "ip.register_magics(PyboardMagic)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## pyboard"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:35.126401Z",
     "start_time": "2020-05-07T07:35:35.105824Z"
    }
   },
   "outputs": [],
   "source": [
    "import pyboard\n",
    "pyb = pyboard.Pyboard('/dev/ttyACM0')\n",
    "pyb.enter_raw_repl()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-19T19:11:18.145548Z",
     "start_time": "2020-05-19T19:11:18.137468Z"
    }
   },
   "outputs": [],
   "source": [
    "pyb.exit_raw_repl()\n",
    "pyb.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:38.725924Z",
     "start_time": "2020-05-07T07:35:38.645488Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "import utime\n",
    "import ulab as np\n",
    "\n",
    "def timeit(n=1000):\n",
    "    def wrapper(f, *args, **kwargs):\n",
    "        func_name = str(f).split(' ')[1]\n",
    "        def new_func(*args, **kwargs):\n",
    "            run_times = np.zeros(n, dtype=np.uint16)\n",
    "            for i in range(n):\n",
    "                t = utime.ticks_us()\n",
    "                result = f(*args, **kwargs)\n",
    "                run_times[i] = utime.ticks_diff(utime.ticks_us(), t)\n",
    "            print('{}() execution times based on {} cycles'.format(func_name, n, (delta2-delta1)/n))\n",
    "            print('\\tbest: %d us'%np.min(run_times))\n",
    "            print('\\tworst: %d us'%np.max(run_times))\n",
    "            print('\\taverage: %d us'%np.mean(run_times))\n",
    "            print('\\tdeviation: +/-%.3f us'%np.std(run_times))            \n",
    "            return result\n",
    "        return new_func\n",
    "    return wrapper\n",
    "\n",
    "def timeit(f, *args, **kwargs):\n",
    "    func_name = str(f).split(' ')[1]\n",
    "    def new_func(*args, **kwargs):\n",
    "        t = utime.ticks_us()\n",
    "        result = f(*args, **kwargs)\n",
    "        print('execution time: ', utime.ticks_diff(utime.ticks_us(), t), ' us')\n",
    "        return result\n",
    "    return new_func"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__END_OF_DEFS__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Universal functions\n",
    "\n",
    "Standard mathematical functions can be calculated on any scalar,  scalar-valued iterable (ranges, lists, tuples containing numbers), and on `ndarray`s without having to change the call signature. In all cases the functions return a new `ndarray` of typecode `float` (since these functions usually generate float values, anyway). The functions execute faster with `ndarray` arguments than with iterables, because the values of the input vector can be extracted faster. \n",
    "\n",
    "At present, the following functions are supported:\n",
    "\n",
    "`acos`, `acosh`, `arctan2`, `around`, `asin`, `asinh`, `atan`, `arctan2`, `atanh`, `ceil`, `cos`, `degrees`, `exp`, `expm1`, `floor`, `log`, `log10`, `log2`, `radians`, `sin`, `sinh`, `sqrt`, `tan`, `tanh`.\n",
    "\n",
    "These functions are applied element-wise to the arguments, thus, e.g., the exponential of a matrix cannot be calculated in this way."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T19:11:07.579601Z",
     "start_time": "2021-01-13T19:11:07.554672Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t range(0, 9)\n",
      "exp(a):\t array([1.0, 2.718281828459045, 7.38905609893065, 20.08553692318767, 54.59815003314424, 148.4131591025766, 403.4287934927351, 1096.633158428459, 2980.957987041728], dtype=float64)\n",
      "\n",
      "=============\n",
      "b:\n",
      " array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], dtype=float64)\n",
      "exp(b):\n",
      " array([1.0, 2.718281828459045, 7.38905609893065, 20.08553692318767, 54.59815003314424, 148.4131591025766, 403.4287934927351, 1096.633158428459, 2980.957987041728], dtype=float64)\n",
      "\n",
      "=============\n",
      "c:\n",
      " array([[0.0, 1.0, 2.0],\n",
      "       [3.0, 4.0, 5.0],\n",
      "       [6.0, 7.0, 8.0]], dtype=float64)\n",
      "exp(c):\n",
      " array([[1.0, 2.718281828459045, 7.38905609893065],\n",
      "       [20.08553692318767, 54.59815003314424, 148.4131591025766],\n",
      "       [403.4287934927351, 1096.633158428459, 2980.957987041728]], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = range(9)\n",
    "b = np.array(a)\n",
    "\n",
    "# works with ranges, lists, tuples etc.\n",
    "print('a:\\t', a)\n",
    "print('exp(a):\\t', np.exp(a))\n",
    "\n",
    "# with 1D arrays\n",
    "print('\\n=============\\nb:\\n', b)\n",
    "print('exp(b):\\n', np.exp(b))\n",
    "\n",
    "# as well as with matrices\n",
    "c = np.array(range(9)).reshape((3, 3))\n",
    "print('\\n=============\\nc:\\n', c)\n",
    "print('exp(c):\\n', np.exp(c))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Computation expenses\n",
    "\n",
    "The overhead for calculating with micropython iterables is quite significant: for the 1000 samples below, the difference is more than 800 microseconds, because internally the function has to create the `ndarray` for the output, has to fetch the iterable's items of unknown type, and then convert them to floats. All these steps are skipped for `ndarray`s, because these pieces of information are already known. \n",
    "\n",
    "Doing the same with `list` comprehension requires 30 times more time than with the `ndarray`, which would become even more, if we converted the resulting list to an `ndarray`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:35:45.696282Z",
     "start_time": "2020-05-07T07:35:45.629909Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "iterating over ndarray in ulab\r\n",
      "execution time:  441  us\r\n",
      "\r\n",
      "iterating over list in ulab\r\n",
      "execution time:  1266  us\r\n",
      "\r\n",
      "iterating over list in python\r\n",
      "execution time:  11379  us\r\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "import math\n",
    "\n",
    "a = [0]*1000\n",
    "b = np.array(a)\n",
    "\n",
    "@timeit\n",
    "def timed_vector(iterable):\n",
    "    return np.exp(iterable)\n",
    "\n",
    "@timeit\n",
    "def timed_list(iterable):\n",
    "    return [math.exp(i) for i in iterable]\n",
    "\n",
    "print('iterating over ndarray in ulab')\n",
    "timed_vector(b)\n",
    "\n",
    "print('\\niterating over list in ulab')\n",
    "timed_vector(a)\n",
    "\n",
    "print('\\niterating over list in python')\n",
    "timed_list(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## arctan2\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.arctan2.html\n",
    "\n",
    "The two-argument inverse tangent function is also part of the `vector` sub-module. The function implements broadcasting as discussed in the section on `ndarray`s. Scalars (`micropython` integers or floats) are also allowed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T19:15:08.215912Z",
     "start_time": "2021-01-13T19:15:08.189806Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\n",
      " array([1.0, 2.2, 33.33, 444.444], dtype=float64)\n",
      "\n",
      "arctan2(a, 1.0)\n",
      " array([0.7853981633974483, 1.14416883366802, 1.5408023243361, 1.568546328341769], dtype=float64)\n",
      "\n",
      "arctan2(1.0, a)\n",
      " array([0.7853981633974483, 0.426627493126876, 0.02999400245879636, 0.002249998453127392], dtype=float64)\n",
      "\n",
      "arctan2(a, a): \n",
      " array([0.7853981633974483, 0.7853981633974483, 0.7853981633974483, 0.7853981633974483], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2.2, 33.33, 444.444])\n",
    "print('a:\\n', a)\n",
    "print('\\narctan2(a, 1.0)\\n', np.arctan2(a, 1.0))\n",
    "print('\\narctan2(1.0, a)\\n', np.arctan2(1.0, a))\n",
    "print('\\narctan2(a, a): \\n', np.arctan2(a, a))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## around\n",
    "\n",
    "`numpy`: https://docs.scipy.org/doc/numpy-1.17.0/reference/generated/numpy.around.html\n",
    "\n",
    "`numpy`'s `around` function can also be found in the `vector` sub-module. The function implements the `decimals` keyword argument with default value `0`. The first argument must be an `ndarray`. If this is not the case, the function raises a `TypeError` exception. Note that `numpy` accepts general iterables. The `out` keyword argument known from `numpy` is not accepted. The function always returns an ndarray of type `mp_float_t`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T19:19:46.728823Z",
     "start_time": "2021-01-13T19:19:46.703348Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a:\t\t array([1.0, 2.2, 33.33, 444.444], dtype=float64)\n",
      "\n",
      "decimals = 0\t array([1.0, 2.0, 33.0, 444.0], dtype=float64)\n",
      "\n",
      "decimals = 1\t array([1.0, 2.2, 33.3, 444.4], dtype=float64)\n",
      "\n",
      "decimals = -1\t array([0.0, 0.0, 30.0, 440.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "a = np.array([1, 2.2, 33.33, 444.444])\n",
    "print('a:\\t\\t', a)\n",
    "print('\\ndecimals = 0\\t', np.around(a, decimals=0))\n",
    "print('\\ndecimals = 1\\t', np.around(a, decimals=1))\n",
    "print('\\ndecimals = -1\\t', np.around(a, decimals=-1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Vectorising generic python functions\n",
    "\n",
    "`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html\n",
    "\n",
    "The examples above use factory functions. In fact, they are nothing but the vectorised versions of the standard mathematical functions. User-defined `python` functions can also be vectorised by help of `vectorize`. This function takes a positional argument, namely, the `python` function that you want to vectorise, and a non-mandatory keyword argument, `otypes`, which determines the `dtype` of the output array. The `otypes` must be `None` (default), or any of the `dtypes` defined in `ulab`. With `None`, the output is automatically turned into a float array. \n",
    "\n",
    "The return value of `vectorize` is a `micropython` object that can be called as a standard function, but which now accepts either a scalar, an `ndarray`, or a generic `micropython` iterable as its sole argument. Note that the function that is to be vectorised must have a single argument."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T19:16:55.709617Z",
     "start_time": "2021-01-13T19:16:55.688222Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "f on a scalar:       array([1936.0], dtype=float64)\n",
      "f on an ndarray:     array([1.0, 4.0, 9.0, 16.0], dtype=float64)\n",
      "f on a list:         array([4.0, 9.0, 16.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "def f(x):\n",
    "    return x*x\n",
    "\n",
    "vf = np.vectorize(f)\n",
    "\n",
    "# calling with a scalar\n",
    "print('{:20}'.format('f on a scalar: '), vf(44.0))\n",
    "\n",
    "# calling with an ndarray\n",
    "a = np.array([1, 2, 3, 4])\n",
    "print('{:20}'.format('f on an ndarray: '), vf(a))\n",
    "\n",
    "# calling with a list\n",
    "print('{:20}'.format('f on a list: '), vf([2, 3, 4]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As mentioned, the `dtype` of the resulting `ndarray` can be specified via the `otypes` keyword. The value is bound to the function object that `vectorize` returns, therefore, if the same function is to be vectorised with different output types, then for each type a new function object must be created."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2021-01-13T19:19:36.090837Z",
     "start_time": "2021-01-13T19:19:36.069088Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "output is uint8:     array([1, 4, 9, 16], dtype=uint8)\n",
      "output is float:     array([1.0, 4.0, 9.0, 16.0], dtype=float64)\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "l = [1, 2, 3, 4]\n",
    "def f(x):\n",
    "    return x*x\n",
    "\n",
    "vf1 = np.vectorize(f, otypes=np.uint8)\n",
    "vf2 = np.vectorize(f, otypes=np.float)\n",
    "\n",
    "print('{:20}'.format('output is uint8: '), vf1(l))\n",
    "print('{:20}'.format('output is float: '), vf2(l))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `otypes` keyword argument cannot be used for type coercion: if the function evaluates to a float, but `otypes` would dictate an integer type, an exception will be raised:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-06T22:21:43.616220Z",
     "start_time": "2020-05-06T22:21:43.601280Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "integer list:        array([1, 4, 9, 16], dtype=uint8)\n",
      "\n",
      "Traceback (most recent call last):\n",
      "  File \"/dev/shm/micropython.py\", line 14, in <module>\n",
      "TypeError: can't convert float to int\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -unix 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "int_list = [1, 2, 3, 4]\n",
    "float_list = [1.0, 2.0, 3.0, 4.0]\n",
    "def f(x):\n",
    "    return x*x\n",
    "\n",
    "vf = np.vectorize(f, otypes=np.uint8)\n",
    "\n",
    "print('{:20}'.format('integer list: '), vf(int_list))\n",
    "# this will raise a TypeError exception\n",
    "print(vf(float_list))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Benchmarks\n",
    "\n",
    "It should be pointed out that the `vectorize` function produces the pseudo-vectorised version of the `python` function that is fed into it, i.e., on the C level, the same `python` function is called, with the all-encompassing `mp_obj_t` type arguments, and all that happens is that the `for` loop in `[f(i) for i in iterable]` runs purely in C. Since type checking and type conversion in `f()` is expensive, the speed-up is not so spectacular as when iterating over an `ndarray` with a factory function: a gain of approximately 30% can be expected, when a native `python` type (e.g., `list`) is returned by the function, and this becomes around 50% (a factor of 2), if conversion to an `ndarray` is also counted.\n",
    "\n",
    "The following code snippet calculates the square of a 1000 numbers with the vectorised function (which returns an `ndarray`), with `list` comprehension, and with `list` comprehension followed by conversion to an `ndarray`. For comparison, the execution time is measured also for the case, when the square is calculated entirely in `ulab`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-05-07T07:32:20.048553Z",
     "start_time": "2020-05-07T07:32:19.951851Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "vectorised function\r\n",
      "execution time:  7237  us\r\n",
      "\r\n",
      "list comprehension\r\n",
      "execution time:  10248  us\r\n",
      "\r\n",
      "list comprehension + ndarray conversion\r\n",
      "execution time:  12562  us\r\n",
      "\r\n",
      "squaring an ndarray entirely in ulab\r\n",
      "execution time:  560  us\r\n",
      "\n"
     ]
    }
   ],
   "source": [
    "%%micropython -pyboard 1\n",
    "\n",
    "from ulab import numpy as np\n",
    "\n",
    "def f(x):\n",
    "    return x*x\n",
    "\n",
    "vf = np.vectorize(f)\n",
    "\n",
    "@timeit\n",
    "def timed_vectorised_square(iterable):\n",
    "    return vf(iterable)\n",
    "\n",
    "@timeit\n",
    "def timed_python_square(iterable):\n",
    "    return [f(i) for i in iterable]\n",
    "\n",
    "@timeit\n",
    "def timed_ndarray_square(iterable):\n",
    "    return np.array([f(i) for i in iterable])\n",
    "\n",
    "@timeit\n",
    "def timed_ulab_square(ndarray):\n",
    "    return ndarray**2\n",
    "\n",
    "print('vectorised function')\n",
    "squares = timed_vectorised_square(range(1000))\n",
    "\n",
    "print('\\nlist comprehension')\n",
    "squares = timed_python_square(range(1000))\n",
    "\n",
    "print('\\nlist comprehension + ndarray conversion')\n",
    "squares = timed_ndarray_square(range(1000))\n",
    "\n",
    "print('\\nsquaring an ndarray entirely in ulab')\n",
    "a = np.array(range(1000))\n",
    "squares = timed_ulab_square(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "From the comparisons above, it is obvious that `python` functions should only be vectorised, when the same effect cannot be gotten in `ulab` only. However, although the time savings are not significant, there is still a good reason for caring about vectorised functions. Namely, user-defined `python` functions become universal, i.e., they can accept generic iterables as well as `ndarray`s as their arguments. A vectorised function is still a one-liner, resulting in transparent and elegant code.\n",
    "\n",
    "A final comment on this subject: the `f(x)` that we defined is a *generic* `python` function. This means that it is not required that it just crunches some numbers. It has to return a number object, but it can still access the hardware in the meantime. So, e.g., \n",
    "\n",
    "```python\n",
    "\n",
    "led = pyb.LED(2)\n",
    "\n",
    "def f(x):\n",
    "    if x < 100:\n",
    "        led.toggle()\n",
    "    return x*x\n",
    "```\n",
    "\n",
    "is perfectly valid code."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "382.797px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
